package com.studiofortress.sf.graphics.display;

import java.awt.Component;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.util.HashSet;

/**
 * An implimentation of Controls which implements the back-end to all the
 * abstract control methods.
 *
 * This is simply storing the values from the various mouse and key listeners
 * that are attached to the canvas of the given GraphicsDisplay.
 *
 * The values are queued internally so that control state at the beginning of a
 * frame is the same as at the end of the same frame. In between each frame the
 * update() method should be called to update the current state of the controls.
 */
class DisplayControls implements Controls
{
    // the real and current state of the controls
    private boolean[] cachedMouseDown;
    private boolean[] cachedMouseClick;
    private HashSet<String> cachedKeys;
    private int cachedMouseX;
    private int cachedMouseY;
    private int cachedMouseZ;

    // the state of the controls,
    // these values are returned when control state is checked
    private boolean[] mouseDown;
    private boolean[] mouseClick;
    private HashSet<String> keys;
    private int mouseX;
    private int mouseY;
    private int mouseZ;

    /**
     * Wraps the canvas inside the given display.
     * @param display The display that contains a canvas for this to pickup controls from.
     */
    DisplayControls(GraphicsDisplay display)
    {
        this((Component) display.getCanvas());
    }

    /**
     * @param display The component for this to pickup the controls from.
     */
    DisplayControls(Component display)
    {
        super();

        if (display == null) {
            throw new IllegalArgumentException("Display cannot be null.");
        }
        setupControlListeners(display);
        
        mouseDown = new boolean[3];
        cachedMouseDown = new boolean[3];
        mouseClick = new boolean[3];
        cachedMouseClick = new boolean[3];
        keys = new HashSet<String>();
        cachedKeys = new HashSet<String>();

        // it's a little easier for apps if they presume the mouse starts in the
        // centre, rather then at 0x0.
        cachedMouseX = display.getWidth()/2;
        mouseX = cachedMouseX;
        cachedMouseY = display.getHeight()/2;
        mouseY = cachedMouseY;
    }

    /**
     * Creates and sets-up the listeners for the
     * windows associated Controls object.
     */
    private final void setupControlListeners(final Component display)
    {
        // listener for mouse movement
        MouseMotionListener mouseMovement = new MouseMotionListener() {
            // mouse movement
            @Override
            public void mouseDragged(MouseEvent e) {
                DisplayControls.this.recordMouseLocation(e.getX(), display.getHeight()-e.getY());
            }
            // mouse movement
            @Override
            public void mouseMoved(MouseEvent e) {
                DisplayControls.this.recordMouseLocation(e.getX(), display.getHeight()-e.getY());
            }
        };

        // listener for mouse input
        MouseListener mouseInput = new MouseListener() {
            // not used
            @Override
            public void mouseClicked(MouseEvent e) {
//                controls.setMouseClicked(e);
            }

            // mouse down
            @Override
            public void mousePressed(MouseEvent e) {
                DisplayControls.this.recordMousePressed(e);
            }

            // mouse up
            @Override
            public void mouseReleased(MouseEvent e) {
                DisplayControls.this.recordMouseReleased(e);
            }

            // not used
            @Override
            public void mouseEntered(MouseEvent e) { }

            // not used
            @Override
            public void mouseExited(MouseEvent e) { }
        };

        // listener for mouse wheel
        MouseWheelListener mouseWheel = new MouseWheelListener() {
            // mouse wheel moved
            @Override
            public void mouseWheelMoved(MouseWheelEvent e) {
                DisplayControls.this.recordMouseWheelRotation(e.getWheelRotation());
            }
        };

        // listener for keyboard input
        KeyListener keyInput = new KeyListener() {
            @Override
            public void keyPressed(KeyEvent e) {
                DisplayControls.this.recordKeyPressed(e);
            }

            @Override
            public void keyReleased(KeyEvent e) {
                DisplayControls.this.recordKeyReleased(e);
            }

            // not used
            @Override
            public void keyTyped(KeyEvent e) { }
        };

        display.addMouseWheelListener(mouseWheel);
        display.addMouseListener(mouseInput);
        display.addMouseMotionListener(mouseMovement);
        display.addKeyListener(keyInput);
    }

    /**
     *
     */
    private void recordMousePressed(MouseEvent e)
    {
        if (e.getButton() == MouseEvent.BUTTON1) {
            cachedMouseDown[MOUSE_LEFT] = true;
            if ( !mouseDown[MOUSE_LEFT] ) {
                cachedMouseClick[MOUSE_LEFT] = true;
            }
        } else if (e.getButton() == MouseEvent.BUTTON3) {
            cachedMouseDown[MOUSE_RIGHT] = true;
            if ( !mouseDown[MOUSE_RIGHT] ) {
                cachedMouseClick[MOUSE_RIGHT] = true;
            }
        } else if (e.getButton() == MouseEvent.BUTTON2) {
            cachedMouseDown[MOUSE_MIDDLE] = true;
            if ( !mouseDown[MOUSE_MIDDLE] ) {
                cachedMouseClick[MOUSE_MIDDLE] = true;
            }
        }
    }
    
    /**
     *
     */
    private void recordMouseReleased(MouseEvent e)
    {
        if (e.getButton() == MouseEvent.BUTTON1) {
            cachedMouseDown[MOUSE_LEFT] = false;
        } else if (e.getButton() == MouseEvent.BUTTON3) {
            cachedMouseDown[MOUSE_RIGHT] = false;
        } else if (e.getButton() == MouseEvent.BUTTON2) {
            cachedMouseDown[MOUSE_MIDDLE] = false;
        }
    }

    /**
     *
     */
    private void recordMouseLocation(int mouseX, int mouseY)
    {
        this.cachedMouseX = mouseX;
        this.cachedMouseY = mouseY;
    }

    /**
     *
     */
    private void recordMouseWheelRotation(int mouseZIncriment)
    {
        cachedMouseZ += mouseZIncriment;
    }

    /**
     *
     */
    private void recordKeyPressed(KeyEvent e)
    {
        cachedKeys.add(KeyEvent.getKeyText(e.getKeyCode()).toLowerCase());
    }

    /**
     *
     */
    private void recordKeyReleased(KeyEvent e)
    {
        cachedKeys.remove(KeyEvent.getKeyText(e.getKeyCode()).toLowerCase());
    }

    /**
     * Checks if the given key has been pressed.
     * If so, will return true, otherwise fale.
     * The string for the key,
     */
    @Override
    public boolean isKeyDown(String key)
    {
        return keys.contains(key.toLowerCase());
    }

    @Override
    public boolean isKeyDown(int keyCode)
    {
        return isKeyDown( KeyEvent.getKeyText(keyCode) );
    }

    @Override
    public boolean isAnyKeyDown()
    {
        return keys.size() > 0;
    }

    /**
     * Returns the last known X position
     * of the mouse.
     */
    @Override
    public int getMouseX()
    {
        return mouseX;
    }

    /**
     * Returns the last known Y position
     * of the mouse.
     * @return the mouse Y position.
     */
    @Override
    public int getMouseY()
    {
        return mouseY;
    }

    /**
     * Returns the last known position
     * of the mouse wheel.
     * @return the current position of the mouse wheel.
     */
    @Override
    public int getMouseZ()
    {
        return mouseZ;
    }

    /**
     * @parms mouseButton the mouse button to be checked.
     * @return true if the mouse button is down, false if not.
     */
    @Override
    public boolean isMouseDown(int mouseButton)
    {
        if (invalidMouseButton(mouseButton)) {
            throw new IllegalArgumentException("MouseButton given is not a valid button.");
        } else {
            return mouseDown[mouseButton];
        }
    }

    /**
     * @parms mouseButton the mouse button to be checked.
     * @return true if the mouse button is clicked, false if not.
     */
    @Override
    public boolean isMouseClick(int mouseButton)
    {
        if (invalidMouseButton(mouseButton)) {
            throw new IllegalArgumentException("MouseButton given is not a valid button.");
        } else {
            return mouseClick[mouseButton];
        }
    }

    /**
     * @return True if any of the mouse buttons are currently pressed.
     */
    @Override
    public boolean isAnyMouseDown()
    {
        for (int i = 0; i < mouseDown.length; i++) {
            if (mouseDown[i]) {
                return true;
            }
        }

        return false;
    }

    /**
     * Return true if the given integer represents a valid
     * mouse button. False if not.
     *
     * @param mouseButton an integer representing a mouse button.
     * @return True if the integer given is a valid mouse button. False if not.
     */
    private boolean invalidMouseButton(int mouseButton)
    {
        return mouseButton != MOUSE_LEFT && mouseButton != MOUSE_RIGHT && mouseButton != MOUSE_MIDDLE;
    }

    /**
     * Updates all of the controls stored to the most recently recorded state.
     * Typically this is called directly before or after the main loop.
     */
    synchronized void update()
    {
        // copy mice state across and clear the mouse clicked for next frame
        for ( int i = 0; i < cachedMouseDown.length; i++ ) {
            mouseDown[i] = cachedMouseDown[i];
            mouseClick[i] = cachedMouseClick[i];
            cachedMouseClick[i] = false;
        }
        
        keys = (HashSet<String>) cachedKeys.clone();
        mouseX = cachedMouseX;
        mouseY = cachedMouseY;
        mouseZ = cachedMouseZ;
    }
}
